package marlon.sjf;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.Enumeration;
import java.util.InvalidPropertiesFormatException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class SJFServer {
	// use STDERR instead of System.out or System.out,
	// because they will be redirected to client sock
	public static final PrintStream STDERR = System.err;
	
	private String host;
	private int port;
	
	private Executor executor;
	
	public SJFServer(String host, int port) {
		this.host = host;
		this.port = port;
		executor = Executors.newFixedThreadPool(10);
	}
	
	public SJFServer() {
		this(SJFConstants.DEFAULT_HOST, SJFConstants.DEFAULT_PORT);
	}
	
	public void startServer() throws IOException  {
		ServerSocket serverSocket = new ServerSocket();
		serverSocket.setReuseAddress(true);
		serverSocket.bind(new InetSocketAddress(host, port));
		initServer();
		STDERR.println("server started on " + serverSocket.getLocalSocketAddress());

		while (true) {
			Socket sock = serverSocket.accept();
			executor.execute(new SJFSession(sock));
		}
	}
	
	private void initServer() {
		redirectStdio();
		
		// set ThreadLocal system properties so every client has it own
		ThreadLocalProperties props = new ThreadLocalProperties(SJFThreadLocals.SYSPROPS);
		props.set(System.getProperties());
		System.setProperties(props);
		
		// set security manager
		
	}
	
	private void redirectStdio() {
		// redirect stdin
		ThreadLocalInputStream stdin = new ThreadLocalInputStream(SJFThreadLocals.STDIN);
		stdin.set(System.in);
		System.setIn(stdin);
		// redirect stdout
		ThreadLocalOutputStream stdout = new ThreadLocalOutputStream(SJFThreadLocals.STDOUT);
		stdout.set(System.out);
		System.setOut(new PrintStream(stdout));
		// redirect stderr
		ThreadLocalOutputStream stderr = new ThreadLocalOutputStream(SJFThreadLocals.STDERR);
		stderr.set(System.err);
		System.setErr(new PrintStream(stderr));
	}
	
	public static void main(String[] args) throws IOException {
		String host = SJFConstants.DEFAULT_HOST;
		int port = SJFConstants.DEFAULT_PORT;
		try {
			for (int i = 0; i < args.length; i++) {
				if (args[i].equals("--help")) {
					printUsage();
					System.exit(0);
				} else if (args[i].equals("-h")) {
					host = args[++i];
				} else if (args[i].equals("-p")) {
					port = Integer.parseInt(args[++i]);
				} else if (args[i].startsWith("-")) {
					STDERR.println("unknown options " + args[i]);
					System.exit(0);
				} else {
					STDERR.print("no args required");
					System.exit(0);
				}
			}
		} catch (Exception e) {
			printUsage();
			System.exit(0);
		}
		SJFServer server = new SJFServer(host, port);
		server.startServer();
	}
	
	private static void printUsage() {
		STDERR.println(
			"Usage: java marlon.sjf.SJFServer [options]\n" +
			"\n" +
			"  --help            show this help message\n" +
			"  -h <host>         specify listen address, default 127.0.0.1\n" +
			"  -p <port>         specify listen port, default 0x2304"
		);
	}
	
	static class ThreadLocalInputStream extends InputStream {
		private ThreadLocal<InputStream> threadLocalIn;
		
		public ThreadLocalInputStream(ThreadLocal<InputStream> threadLocalIn) {
			this.threadLocalIn = threadLocalIn;
		}
		
		public void set(InputStream in) {
			threadLocalIn.set(in);
		}
		
		public InputStream get() {
			return threadLocalIn.get();
		}

		@Override
		public int read() throws IOException {
			return get().read();
		}

		@Override
		public int read(byte[] b) throws IOException {
			return get().read(b);
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			return get().read(b, off, len);
		}

		@Override
		public long skip(long n) throws IOException {
			return get().skip(n);
		}

		@Override
		public int available() throws IOException {
			return get().available();
		}

		@Override
		public void close() throws IOException {
			get().close();
		}

		@Override
		public void mark(int readlimit) {
			get().mark(readlimit);
		}

		@Override
		public void reset() throws IOException {
			get().reset();
		}

		@Override
		public boolean markSupported() {
			return get().markSupported();
		}
	}
	
	static class ThreadLocalOutputStream extends OutputStream {
		private ThreadLocal<OutputStream> threadLocalOut;
		
		public ThreadLocalOutputStream(ThreadLocal<OutputStream> threadLocalOut) {
			this.threadLocalOut = threadLocalOut;
		}
		
		public void set(OutputStream out) {
			threadLocalOut.set(out);
		}
		
		public OutputStream get() {
			return threadLocalOut.get();
		}

		@Override
		public void write(int b) throws IOException {
			get().write(b);
		}

		@Override
		public void write(byte[] b) throws IOException {
			get().write(b);
		}

		@Override
		public void write(byte[] b, int off, int len) throws IOException {
			get().write(b, off, len);
		}

		@Override
		public void flush() throws IOException {
			get().flush();
		}

		@Override
		public void close() throws IOException {
			get().close();
		}
	}

	static class ThreadLocalProperties extends Properties {
		private ThreadLocal<Properties> threadLocalProps;
		
		public ThreadLocalProperties(ThreadLocal<Properties> tlProps) {
			threadLocalProps = tlProps;
		}
		
		public void set(Properties props) {
			threadLocalProps.set(props);
		}
		
		public Properties get() {
			return threadLocalProps.get();
		}
		
		// BEGIN Properties methods
		@Override
		public Object setProperty(String key, String value) {
			return get().setProperty(key, value);
		}

		@Override
		public void load(Reader reader) throws IOException {
			get().load(reader);
		}

		@Override
		public void load(InputStream inStream) throws IOException {
			get().load(inStream);
		}

		@SuppressWarnings("deprecation")
		@Override
		public void save(OutputStream out, String comments) {
			get().save(out, comments);
		}

		@Override
		public void store(Writer writer, String comments) throws IOException {
			get().store(writer, comments);
		}

		@Override
		public void store(OutputStream out, String comments) throws IOException {
			get().store(out, comments);
		}

		@Override
		public void loadFromXML(InputStream in)
				throws IOException, InvalidPropertiesFormatException {
			get().loadFromXML(in);
		}

		@Override
		public void storeToXML(OutputStream os, String comment)
				throws IOException {
			get().storeToXML(os, comment);
		}

		@Override
		public void storeToXML(OutputStream os, String comment,
				String encoding) throws IOException {
			get().storeToXML(os, comment, encoding);
		}

		@Override
		public String getProperty(String key) {
			return get().getProperty(key);
		}

		@Override
		public String getProperty(String key, String defaultValue) {
			return get().getProperty(key, defaultValue);
		}

		@Override
		public Enumeration<?> propertyNames() {
			return get().propertyNames();
		}

		@Override
		public Set<String> stringPropertyNames() {
			return get().stringPropertyNames();
		}

		@Override
		public void list(PrintStream out) {
			get().list(out);
		}

		@Override
		public void list(PrintWriter out) {
			get().list(out);
		}
		// END Properties methods

		// BEGIN Hashtable methods
		@Override
		public int size() {
			return get().size();
		}

		@Override
		public boolean isEmpty() {
			return get().isEmpty();
		}

		@Override
		public synchronized Enumeration<Object> keys() {
			return get().keys();
		}

		@Override
		public synchronized Enumeration<Object> elements() {
			return get().elements();
		}

		@Override
		public synchronized boolean contains(Object value) {
			return get().contains(value);
		}

		@Override
		public boolean containsValue(Object value) {
			return get().containsValue(value);
		}

		@Override
		public synchronized boolean containsKey(Object key) {
			return get().containsKey(key);
		}

		@Override
		public synchronized Object get(Object key) {
			return get().get(key);
		}

		@Override
		public synchronized Object put(Object key, Object value) {
			return get().put(key, value);
		}

		@Override
		public synchronized Object remove(Object key) {
			return get().remove(key);
		}

		@Override
		public synchronized void putAll(
				Map<? extends Object, ? extends Object> t) {
			get().putAll(t);
		}

		@Override
		public synchronized void clear() {
			get().clear();
		}

		@Override
		public synchronized Object clone() {
			return get().clone();
		}

		@Override
		public Set<Object> keySet() {
			return get().keySet();
		}

		@Override
		public Set<Entry<Object, Object>> entrySet() {
			return get().entrySet();
		}

		@Override
		public Collection<Object> values() {
			return get().values();
		}

		@Override
		public synchronized String toString() {
			return get().toString();
		}
		// END Hashtable method
	}
}
